什么是事件循环(event loop)?
尽管js是单线程的,事件循环机制,通过在合适的时候把操作交给系统内核,从而允许node执行非阻塞的io操作
当操作完成时,内核告知node.js,合适的回调函数会被加入轮询队列,最终被执行。
Node.js启动的时候,初始化event loop,处理提供的脚本,脚本中可能调用异步API,调度timers,或者调用process.nextTick(),然后处理event loop
下图是简化的事件循环操作顺序图overview
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
图中每个box就是一个phase,每个phase有一个先进先出的回调函数的队列,
event loop进入了一个phase,就会执行phase中所有的操作,然后执行回调函数,直到队列耗尽了,或者回调函数执行数量到达最大数,接下来就去下一个phase
因为任何一个操作都可能调度更多的操作,而且poll phase中新的事件由内核排队,所以正在轮询的事件在被处理的时候,poll事件们可能会排队。
结果:长时间的运行回调函数允许poll phase运行事件比timer的阈值更长。
phase overview 阶段概况
timers:执行由setTimeout() and setInterval()调度的回调函数
I/O callbacks:执行所有的回调函数,除了 close callbacks(由timers,setImmediate()调度)
idle, prepare:内部使用
poll:获取新的io事件,当合适的时候,node会阻塞在这里
check: setImmediate()回调函数会在这里调用
close callbacks: e.g. socket.on('close', ...)
每次运行event loop,node检查是否有对任何异步io或者timers的等待,没有就关闭
Phases in Detail(各阶段细述)
timers
timers指定阈值(threshold)之后,会执行回调函数,但threshold不是执行回调函数的确切时间(只是最短时间)。
timers回调函数一旦可以执行了就会被执行。然而操作系统的调度或者其他的回调函数可能推迟它的执行。
由poll phase来控制什么时候timers被执行
var fs = require('fs');
function someAsyncOperation (callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
var timeoutScheduled = Date.now();
setTimeout(function () {
var delay = Date.now() - timeoutScheduled;
console.log(delay + "ms have passed since I was scheduled");
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(function () {
var startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
; // do nothing
}
});
一开始timer被调度,里面的回调函数执行log。
然后事件循环进入poll phase,此时队列是空的(因为fs.readFile()没有完成),所以就会等着,直到最早的timer的阈值(100)到时间,等了95 ms(还没到,毕竟定的是100),fs.readFile() 这个时候完成了,所以它的回调函数就回被加poll的队列并且被执行(执行10s),当回调函数完成了,队列又空了,所以,event loop将会看到timer的阈值(100)已经到了,
然后回到timers这个phase去执行timers的回调函数,也就是,打印出105秒
为了防止poll phase 独占耗尽 event loop,libuv 也有一个最大值(基于系统),会在超过最大值之前停止轮询更多的事件。
I/O callbacks
为系统操作(比如tcp错误类型)执行回调函数
当tcp socket尝试连接时接收到ECONNREFUSED,类unix系统将会想报道错误,要会在这个phase排队执行。
poll
poll phase有两个功能
为到了时间的timers执行脚本,然后
处理poll队列的事件
当event loop 进入poll phase且没有timers被调度,下面的事情会发生
-
poll不空,
通过回调函数队列迭代的执行
-
poll栈是空的
如果脚本已经被setImmediate()调度,事件循环将会终止poll phase,到check phase去执行那些被调度的脚本
等着回调函数被加进队列,然后立马执行它
一旦poll空了,event loop将回检查timers有没有thresholds到了,有的话,wrap back to the timers phase,然后执行timers的回调函数
check
特别的 timer
close callbacks
setImmediate and setTimeout()
在poll完成以后执行
在最小事件之后执行
执行顺序:
依赖于调用的上下文
-
如果都在main module ,事件会被进程的性能限制(被其他应用影响)
not within an I/O cycle:不确定的
within an I/O cycle:immediate总是先(更好)
// timeout_vs_immediate.js
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
// timeout_vs_immediate.js
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
The Node.js Event Loop, Timers
参考:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。